﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml;
using Notoric.Model.Elements;
using Notoric.Model.Packaging;

namespace Notoric.Model.Collections
{
    /// <summary>
    /// Represents a dynamic data collection of <see cref="ModelElement"/>s that is backed-up by an XmlElement
    /// and provides notifications when items get added, removed, or when the whole list is refreshed.
    /// </summary>
    /// <remarks>
    /// <para>
    /// For implementation reasons this collection cannot contain <see langword="null"/> items.
    /// When an attempt is made to insert a <see langword="null"/> value to the collection, 
    /// an exception is thrown.
    /// </para>
    /// <para>
    /// The collection is able to recreate itself from the serialized XML representation. For this purpose
    /// the XML element of the item inserted into this collection should contain an attribute called "type"
    /// the value of which is the full name of the item type. Otherwise it is added automatically upon
    /// the insertion.
    /// </para>
    /// <para>
    /// When recreating the collection content, the implementation expects the element to have a public
    /// constructor that accepts three parameters: owner package part (<see cref="XmlPackagePart"/>),
    /// owner model element (<see cref="ModelElement"/>) and serialized XML element (<see cref="XmlElement"/>).
    /// </para>
    /// </remarks>
    /// <typeparam name="T">Type of the collection items (must be a subtype of <see cref="ModelElement"/></typeparam>
    public class ElementCollection<T> : ObservableCollection<T>
        where T : ModelElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="ElementCollection{T}"/> instance.
        /// </summary>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ModelElement"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this collection.
        /// </para>
        /// </param>
        /// <param name="collectionElement">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The root element of the collection.<br/>
        /// If the element contains child elements, the collection will attempt 
        /// to recreate its content from these elements.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="owner"/> or <paramref name="collectionElement"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="SerializationException">
        /// The collection failed to recreate its content.
        /// </exception>
        public ElementCollection(ModelElement owner, XmlElement collectionElement)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");
            if (collectionElement == null)
                throw new ArgumentNullException("collectionElement");

            this.owner = owner;
            this.collectionElement = collectionElement;

            // Try to recreate the collection content
            RecreateElements();
        }

        /// <summary>
        /// Creates a new <see cref="ElementCollection{T}"/> instance that contains
        /// elements copied from a given collection.
        /// </summary>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ModelElement"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this collection.
        /// </para>
        /// </param>
        /// <param name="collectionElement">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The root element of the collection.
        /// </para>
        /// </param>
        /// <param name="collection">
        /// <para>
        /// Type: <see cref="IEnumerable{T}"/>
        /// </para>
        /// <para>
        /// The source collection.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="owner"/> or <paramref name="collectionElement"/> or <paramref name="collection"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The <paramref name="collectionElement"/> contains child elements.
        /// </exception>
        public ElementCollection(ModelElement owner, XmlElement collectionElement, IEnumerable<T> collection)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");
            if (collectionElement == null)
                throw new ArgumentNullException("collectionElement");
            if (collection == null)
                throw new ArgumentNullException("collection");

            if (collectionElement.ChildNodes.OfType<XmlElement>().Count() > 0)
                throw new ArgumentException("The collectionElement must not contain child elements!", "collectionElement");

            this.collectionElement = collectionElement;

            foreach (T item in collection)
                this.Add(item);
        }

        #endregion

        #region ObservableCollection<T> Members

        /// <inheritdoc/>
        protected override void ClearItems()
        {
            foreach (XmlNode child in collectionElement.ChildNodes)
            {
                if (child.NodeType == XmlNodeType.Element)
                    collectionElement.RemoveChild(child);
            }

            base.ClearItems();
        }

        /// <inheritdoc/>
        protected override void InsertItem(int index, T item)
        {
            CheckAndTypeDecorateItem(item);

            if (index < base.Count)
                collectionElement.InsertBefore(item.Element, base[index].Element);
            else
                collectionElement.AppendChild(item.Element);

            base.InsertItem(index, item);
        }

        /// <inheritdoc/>
        protected override void MoveItem(int oldIndex, int newIndex)
        {
            T item = base[oldIndex];

            collectionElement.RemoveChild(item.Element);
            if (newIndex < base.Count)
                collectionElement.InsertBefore(item.Element, base[newIndex].Element);
            else
                collectionElement.AppendChild(item.Element);

            base.RemoveItem(oldIndex);
            base.InsertItem(newIndex, item);
        }

        /// <inheritdoc/>
        protected override void RemoveItem(int index)
        {
            T oldItem = base[index];

            collectionElement.RemoveChild(oldItem.Element);

            base.RemoveItem(index);
        }

        /// <inheritdoc/>
        protected override void SetItem(int index, T item)
        {
            CheckAndTypeDecorateItem(item);

            T oldItem = base[index];

            collectionElement.ReplaceChild(item.Element, oldItem.Element);

            base.SetItem(index, item);
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Checks that the item is not <see langword="null"/> and adds the type attribute
        /// to its XML element if it has not already contained one.
        /// </summary>
        /// <param name="item">
        /// <para>
        /// Type: <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The item.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="item"/> is <see langword="null"/>.
        /// </exception>
        private void CheckAndTypeDecorateItem(T item)
        {
            if (item == null)
                throw new ArgumentException("This collection cannot contain null values!");
            
            if (item.Element.Attributes["type"] == null)
            {
                var typeAttr = item.Element.OwnerDocument.CreateAttribute("type");
                typeAttr.Value = item.GetType().FullName;
                item.Element.Attributes.Append(typeAttr);
            }
        }

        /// <summary>
        /// Tries to recreate the content of the collection from the XML representation.
        /// </summary>
        /// <exception cref="SerializationException">
        /// The deserialization failed.
        /// </exception>
        private void RecreateElements()
        {
            try
            {
                foreach (XmlNode childNode in collectionElement.ChildNodes)
                {
                    if (childNode.NodeType == XmlNodeType.Element)
                    {
                        var childElement = (XmlElement)childNode;

                        var myAssembly = Assembly.GetExecutingAssembly();
                        var childType = myAssembly.GetType(childElement.Attributes["type"].Value);

                        var child = ModelElement.LoadElement<T>(owner.PackagePart, owner, childElement);
                        Items.Add(child);
                    }
                }
            }
            catch (Exception e)
            {
                throw new SerializationException("Could not recreate the collection content!", collectionElement.OuterXml, e);
            }
        }
        
        #endregion

        #region Internal Properties

        /// <summary>
        /// Gets the XML element that is the root of the collection.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The element that is the root of the collection and contains
        /// the elements of all the collection items.
        /// </para>
        /// </value>
        internal XmlElement CollectionElement
        {
            get { return collectionElement; }
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the model element that directly owns this collection.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ModelElement"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this collection.
        /// </para>
        /// </value>
        public ModelElement Owner
        {
            get { return owner; }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the collection root XML element.
        /// </summary>
        private XmlElement collectionElement;
        /// <summary>
        /// Holds a reference to the model element that directly owns this collection.
        /// </summary>
        private ModelElement owner;

        #endregion
    }
}
